import unittest
__all__ = ['add_OK_test', 'add_OK_suite',
         'add_E_test', 'add_E_suite', 'do_tests']

"""Provide function-oriented light interface to unittest module.

You should just add tests as a set of callables, arguments and 
results expected and then call do_tests() routine to perform
tests planned

e.g.:

#-------------------------------------------------------------------------
#                      routine-under-test

def my_sqrt(x, exception_safe):
    '''function evaluates square root;
    
    input value x should not be
    negative or complex;
    if exception_safe is False and
    x value is not correct,
    exception should raise;
    if exception_safe is True, this
    function never raises an exception
    and return NaN in case of incorrect
    input data
    '''
    # function body
    ...

#-------------------------------------------------------------------------
#                      TESTS (maybe, in separate module)

# Add tests in format: callable, expected result, arguments
# (arguments in positional or keyword form, or mixed).
add_OK_test(my_sqrt, 0, x=0, exception_safe=False)
add_OK_test(my_sqrt, 0, x=0, exception_safe=True)
add_OK_test(my_sqrt, 1, 1, False)
add_OK_test(my_sqrt, NaN, -1, True)
add_OK_test(my_sqrt, NaN, x=-4, exception_safe=True)
add_OK_test(my_sqrt, NaN, '1', exception_safe=True)
add_OK_test(my_sqrt, NaN, '4', True)

# You may also add test suite in format: callable, several lists of
# (expected result, arguments).
add_OK_suite(
            my_sqrt,
            (2, 4, False),
            (2, 4, True),
            (3, 9, False),
            (3, 9, True),
            (Nan, -1, True),
           )

# Add test for call, which should raise an exception,
# format is: callable, exception expected class, arguments.
add_E_test(my_sqrt, ValueError, -1, False)
add_E_test(my_sqrt, TypeError, '1', False)

# You may also add this kind of tests as one suite.
add_E_suite(
           my_sqrt,
           (ValueError, -1, False),
           (TypeError, '4', False)
           (TypeError, (1 + 1j*3), False)
         )

# And, at last, perform all tests planned.
do_tests()
# tests results should be output to console
#   in unittest conventional format
"""

# Test suite as a global variable.
# You may use it as a conventional unittest.TestSuite
# instance or use routines defined in this module to
# add new test cases to the suite and perform testing.
TESTS = unittest.TestSuite()

def quoted_value(value):
    """Enclose variable value by quotes for pretty-print."""
    return '<' + str(value) + '>'

# Idioms used in output messages.
MSG_IDIOMS = {
              'called_with':'call arguments are:',
              'no_args':'called with no arguments',
              'found_retval':'found return value:',
              'expected_retval':'expected return value:',
              'in_module':'in the module:'
             }

class TestCase(unittest.TestCase):
    
    """Test case classes base.
    
    Provide work with a callable using *args, **kwargs syntax
    and test short description.
    """
    
    def __init__(self, callable, args, kwargs):
        """Constructor.
        
        Callable -- routine-under-test;
        args -- list of positional arguments for routine-under-test;
        kwargs -- dictionary of keyword arguments for routine-under-test.
        """
        unittest.TestCase.__init__(self)
        self.__callable = callable
        self.__args = args
        self.__kwargs = kwargs
    
    def runCall(self):
        """Call routine-under-test with arguments specified."""
        return self.__callable(*self.__args, **self.__kwargs)
    
    def shortDescription(self):
        """Return test text description (not so short).
        
        Description includes routine-under-test name
        and arguments list.
        """
        result = ''
        if (hasattr(self.__callable, '__name__') and 
            self.__callable.__name__):
            result += self.__callable.__name__
        elif (hasattr(self.__callable, '__doc__') and 
              self.__callable.__doc__):
            result += self.__callable.__doc__
        else:
            result += str(self.__callable)
        if hasattr(self.__callable, '__module__'):
            result += ('\n' + MSG_IDIOMS['in_module'] +
                       self.__callable.__module__)
        result += '\n' + MSG_IDIOMS['called_with']
        if not (self.__kwargs or self.__args):
            result += '\n' + MSG_IDIOMS['no_args']
            return result
        for arg in self.__args:
            result += '\n' + quoted_value(arg)
        argnames = self.__kwargs.keys()
        argnames.sort()
        for name in argnames:
            result += ('\n' + str(name) + '=' +
                       quoted_value(self.__kwargs[name]))
        return result
    
    def runTest(self):
        """Abstract test method.
        
        The runTest is unittest.TestCase default test name;
        this method is an abstract method and should been overriden
        in child classes.
        """
        raise \
       RuntimeError('runTest method should been overriden in child')

class OKTestCase(TestCase):
    
    """Successive test case class.
    
    This class represents the following test case:
    routine-under-test should return some value
    and should NOT raise an exception.
    Test is successfull iff routine-under-test
    return value is the same as the expected value.
    """
    
    def __init__(self, callable, args, kwargs, result_expected):
        """Constructor.
        
        callable -- routine-under-test;
        args -- list of positional arguments;
        kwargs -- dictionary of keyword arguments;
        result_expected -- result expected
            to be return by callable call.
        """
        TestCase.__init__(self, callable, args, kwargs)
        self.__result_expected = result_expected
    
    def runTest(self):
        """Key method -- is called to perform current test case."""
        result = self.runCall()
        assert self.__result_expected == result, \
            (MSG_IDIOMS['found_retval'] + quoted_value(result) + ';' +
             MSG_IDIOMS['expected_retval'] +
             quoted_value(self.__result_expected))

class ExcTestCase(TestCase):
    
    """Class for test case with exception catch.
    
    This class represents test in which routine-under-test call
    should raise an exception.
    Test is successfull iff routine-under-test call
    raises an exception of the specified type.
    """
    
    def __init__(self, callable, args, kwargs, exception_class_expected):
        """Constructor.
        
        callable -- routine-under-test;
        args -- list of positional arguments;
        kwargs -- dictionary of keyword arguments;
        exception_class_expected -- type of exception expected
            to be raised in callable call.
        """
        TestCase.__init__(self, callable, args, kwargs)
        self.__exception_class_expected = exception_class_expected
    
    def runTest(self):
        """Key method -- is called to perform current test case."""
        self.failUnlessRaises(self.__exception_class_expected,
                              self.runCall)

def add_OK_test(OK_test_tested_callable,\
                OK_test_tested_callable_result_expected,\
                *args, **kwargs):
    """Add a single successive test.
    
    OK_test_tested_callable -- routine-under-test;
    OK_test_tested_callable_result_expected -- result expected to be return
        by callable call;
    other arguments -- arguments for callable call
        (may be in positional form, keyword form or both).
    """
    TESTS.addTest(OKTestCase(OK_test_tested_callable,
                             args, kwargs,
                             OK_test_tested_callable_result_expected))

def add_OK_suite(callable, *cases_data):
    """Add successive tests suit.
    
    Add a suit of tests for one given callable.
    callable -- routine-under-test;
    cases_data -- several lists, each of
    lists starts with result expected, other
    values in list are call arguments, e.g.:
    
    add_OK_suite(my_sqrt, (2, 4), (3, 9))
    
    adds two tests, the first one checks if
        my_sqrt(4) == 2
    the second one checks if
        my_sqrt(9)==3
    """
    for case in cases_data:
        add_OK_test(callable, case[0], *(case[1:]))

def add_E_test(E_test_tested_callable, E_test_exception_class_expected, *args, **kwargs):
    """Add a test, which should fail unless exception raises;
    
    E_test_tested_callable -- routine-under-test;
    E_test_exception_class_expected -- exception expected to raise
        in callable call;
    other arguments -- arguments for callable call
        (may be in positional form, keyword form or both).
    """
    TESTS.addTest(ExcTestCase(E_test_tested_callable, args, kwargs,
                              E_test_exception_class_expected))

def add_E_suite(callable, *cases_data):
    """Add a suit of tests, each test fails unless exception raises.
    
    callable -- routine-under-test;
    cases_data -- several lists, each of
    lists starts with exception class expected, other
    values in list are call arguments, e.g.:
    
    add_E_suite(my_sqrt, (ValueError, -1), (TypeError, '4'))
    
    adds two tests, the first one checks if
        my_sqrt(-1) raises ValueError exception
    the second one checks if
        my_sqrt('4') raises TypeError exception
    """
    for case in cases_data:
        add_E_test(callable, case[0], *(case[1:]))

def do_tests():
    """Perform all tests planned.
    
    Perform all tests, added by add_OK_test, add_OK_suite,
    add_E_test, addEsuit calls.
    """
    runner = unittest.TextTestRunner()
    runner.run(TESTS)
